Custom Thumbnail Bar
You can use the <cylindo-viewer>
with a custom thumbnail bar.
Set the value of the slot
attribute of the custom thumbnail bar to thumbnail-bar
and use the methods and properties on the <cylindo-viewer>
to control the thumbnail bar.
Simple custom thumbnail bar
Below, you will find a simple example to get you started.
This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.
function Example() { const viewer = React.useRef(null); const [, forceUpdate] = useState({}); // We wait for web-component to be registered useEffect(() => { customElements.whenDefined("cylindo-viewer").then(() => forceUpdate({})); }, []); // We force a state change to trigger a re-render, thus observing the properties of the viewer ref. const eventListener = () => forceUpdate({}); // We add our event listeners to the viewer. useEffect(() => { if (viewer.current) { viewer.current.addEventListener("item-change", eventListener); viewer.current.addEventListener("loaded", eventListener); viewer.current.addEventListener("items-change", eventListener); viewer.current.addEventListener("frame-change", eventListener); } }, [viewer.current]); useEffect(() => { return () => { if (viewer.current) { viewer.current.removeEventListener("item-change", eventListener); viewer.current.removeEventListener("loaded", eventListener); viewer.current.removeEventListener("items-change", eventListener); viewer.current.removeEventListener("frame-change", eventListener); } }; }, []); // Access the properties of the viewer needed for the thumbnail bar const { store, items, item } = viewer.current || {}; const { viewerItemIndex } = item || {}; return ( <cylindo-viewer ref={viewer} customer-id="5098" code="ARMCHAIR-PDP" controls="ar qr fullscreen zoom" > <cylindo-studio code="RS-BARILA-A" customer-id="5098" /> <cylindo-360-frame frame="10" /> <cylindo-360-frame frame="16" /> <cylindo-360 frame="3" /> <cylindo-model /> <div slot="thumbnail-bar" className="thumbnail-bar"> {items && items.map((item, index) => { const isSelected = viewerItemIndex === index; { /* Button where you can pass your own design, or you can call the Content API (CAPI) to get content for each item. */ } return ( <button key={index} className={isSelected ? "selected" : ""} onClick={() => (viewer.current.item = { viewerItem: item, viewerItemIndex: index, }) } > {item.type} </button> ); })} </div> </cylindo-viewer> ); }
Custom thumbnail bar with remote config
The following example demonstrates how to build a thumbnail bar for remote config.
This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.
function Example() { const viewer = React.useRef(null); const [, forceUpdate] = useState({}); // We wait for web-component to be registered useEffect(() => { customElements.whenDefined("cylindo-viewer").then(() => forceUpdate({})); }, []); // We force a state change to trigger a re-render, thus observing the properties of the viewer ref. const eventListener = () => forceUpdate({}); // We add our event listeners to the viewer. useEffect(() => { if (viewer.current) { viewer.current.addEventListener("item-change", eventListener); viewer.current.addEventListener("loaded", eventListener); viewer.current.addEventListener("items-change", eventListener); viewer.current.addEventListener("frame-change", eventListener); viewer.current.addEventListener("config-change", eventListener); } }, [viewer.current]); useEffect(() => { return () => { if (viewer.current) { viewer.current.removeEventListener("item-change", eventListener); viewer.current.removeEventListener("loaded", eventListener); viewer.current.removeEventListener("items-change", eventListener); viewer.current.removeEventListener("frame-change", eventListener); viewer.current.removeEventListener("config-change", eventListener); } }; }, []); // Access the properties of the viewer needed for the thumbnail bar const { store, items: localItems, item } = viewer.current || {}; const { viewerItemIndex } = item || {}; // The remote config items can be accessed through the store const { items: remoteConfigItems } = (store && store.get().config) || {}; // Merge local items and remoteConfig items, if they exist. const items = [...(localItems || []), ...(remoteConfigItems || [])]; return ( <cylindo-viewer ref={viewer} customer-id="5098" code="SALSIE FF" remote-config="k2hctc08" controls="ar qr fullscreen zoom" > <div slot="thumbnail-bar" className="thumbnail-bar"> {items && items.map((item, index) => { const isSelected = viewerItemIndex === index; const features = item.features || (viewer.current ? viewer.current.features : {}); { /* Button where you can pass your own design, or you can call the Content API (CAPI) to get content for each item. */ } return ( <button key={index} className={isSelected ? "selected" : ""} onClick={() => (viewer.current.item = { viewerItem: item, viewerItemIndex: index, }) } > {item.type} </button> ); })} </div> </cylindo-viewer> ); }
Custom thumbnail bar with different materials' variations
The example below shows a possible approach to creating a custom thumbnail bar with local items with different materials' variations. Clicking on these items will swap the materials' variations and change the current viewer item.
This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.
function Example() { const viewer = React.useRef(null); const [, forceUpdate] = useState({}); const customerId = 5098; const code = "WHISTLER SOFA BED"; // We wait for web-component to be registered useEffect(() => { customElements.whenDefined("cylindo-viewer").then(() => forceUpdate({})); }, []); // We force a state change to trigger a re-render, thus observing the properties of the viewer ref. const eventListener = () => forceUpdate({}); // We add our event listeners to the viewer. useEffect(() => { if (viewer.current) { viewer.current.addEventListener("item-change", eventListener); viewer.current.addEventListener("loaded", eventListener); viewer.current.addEventListener("features-change", eventListener); viewer.current.addEventListener("items-change", eventListener); viewer.current.addEventListener("frame-change", eventListener); viewer.current.addEventListener("config-change", eventListener); } }, [viewer.current]); useEffect(() => { return () => { if (viewer.current) { viewer.current.removeEventListener("item-change", eventListener); viewer.current.removeEventListener("features-change", eventListener); viewer.current.removeEventListener("loaded", eventListener); viewer.current.removeEventListener("items-change", eventListener); viewer.current.removeEventListener("frame-change", eventListener); viewer.current.removeEventListener("config-change", eventListener); } }; }, []); // Access the properties of the viewer needed for the thumbnail bar const { store, items: localItems, item } = viewer.current || {}; const { viewerItemIndex } = item || {}; // The remote config items can be accessed through the store const { items: remoteConfigItems } = (store && store.get().config) || {}; // Merge local items and remoteConfig items, if they exist. const items = [...(localItems || []), ...(remoteConfigItems || [])]; return ( <cylindo-viewer ref={viewer} customer-id={customerId} code={code} remote-config="k2hctc08" controls="ar qr fullscreen zoom" > <div slot="thumbnail-bar" className="thumbnail-bar"> {items && [ ...items, // Append the custom items with different materials { type: "360", custom: true, features: { UPHOLSTERY: "MONTREAL SAND" }, frame: 1, }, { type: "360", custom: true, features: { UPHOLSTERY: "VICTORY TEAL" }, frame: 16, }, { type: "360StaticFrame", custom: true, features: { UPHOLSTERY: "ELEMENT EMERALD" }, frame: 1, }, ].map((item, index) => { const isSelected = viewerItemIndex === index; const features = item.features || (viewer.current ? viewer.current.features : {}); return ( <button key={index} className={`thumb ${isSelected ? "selected" : ""}`} onClick={() => { // For our appended items if (item.custom) { // Find the corresponding item const _item = items.find(i => i.type === item.type); // Append the new feature (Material variation) viewer.current.features = { ...viewer.current.features, ...item.features, }; // Set the new item viewer.current.item = { viewerItem: _item, viewerItemIndex: index, }; if (item.frame) { viewer.current.frame = item.frame; } return; } viewer.current.item = { viewerItem: item, viewerItemIndex: index, }; }} > <img className="thumb-image" src={getThumbnailImgSrc({ customerId, code, item, features, })} alt={`Thumbnail bar item - ${item.type}`} draggable="false" /> </button> ); })} </div> </cylindo-viewer> ); // Get the thumbs images from the Content API function getThumbnailImgSrc({ item, customerId, code, features }) { const baseUrl = `https://content.cylindo.com/api/v2/${customerId}/products/`; // For the sake of this example, we take only the first feature available UPHOLSTERY. const featureOption = features["UPHOLSTERY"]; const featuresParams = featureOption ? `&feature=UPHOLSTERY:${encodeURIComponent(featureOption)}` : ""; switch (item.type) { case "studio": return `${baseUrl}${item.code}/frames/1/${item.code}.webp?size=105${featuresParams}`; case "360StaticFrame": return `${baseUrl}${code}/frames/${item.frame}/${code}.webp?size=105${featuresParams}`; case "360": return `${baseUrl}${code}/frames/${ item.frame || 1 }/${code}.webp?size=105${featuresParams}`; case "model": return `${baseUrl}${code}/frames/1/${code}.webp?size=105${featuresParams}`; case "dimensionShot": return `${baseUrl}${code}/dimensions/${code}.webp?dimensionCode=${item.dimensionCode}&dimensionLabelUnit=${item.dimensionLabelUnit}&size=105${featuresParams}`; case "swatch": return `${baseUrl}${code}/material/${code}.webp?crop=(32,32,64,64)&size=105&size=105&feature=UPHOLSTERY:${encodeURIComponent( featureOption || item.defaultOptionCode )}`; default: throw Error(`Unhandled item type: ${item.type}`); } } }
The following code shows the styles used for all the examples above.
::part(thumbnail-bar-fullscreen-container) {
display: flex;
justify-content: center;
align-items: center;
padding: 4px 8px;
}
.thumbnail-bar {
display: flex;
gap: 4px;
display: flex;
column-gap: 10px;
margin: 10px 0;
align-items: center;
padding: 0 1em;
}
.thumb {
cursor: pointer;
padding: 0;
background-color: #abafad26;
border: 1px solid transparent;
border-radius: 8px;
padding: 4px;
box-sizing: content-box;
width: 64px;
height: 48px;
flex-shrink: 0;
transition: border 500ms;
}
.selected {
border: 1px solid #9bb0be;
}
.thumb-image {
display: flex;
justify-content: center;
align-items: center;
object-fit: cover;
width: 100%;
height: 100%;
border-radius: 4px;
}
Vertical Thumbnail Bar
This examples shows how to create a custom vertical thumbnail bar.
This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.
function Example() { const EVENTS_FORCE_UPDATE = [ "item-change", "loaded", "items-change", "frame-change", ]; const getThumbnailImgSrc = ({ item, customerId, code }) => { const baseUrl = `https://content.cylindo.com/api/v2/${customerId}/products/`; switch (item.type) { case "studio": return `${baseUrl}${item.code}/frames/1/${item.code}.webp?size=105`; case "360StaticFrame": return `${baseUrl}${code}/frames/${item.frame}/${code}.webp?size=105`; case "360": return `${baseUrl}${code}/frames/3/${code}.webp?size=105`; case "model": return `${baseUrl}${code}/frames/1/${code}.webp?size=105`; case "dimensionShot": return `${baseUrl}${code}/dimensions/${code}.webp?dimensionCode=${item.dimensionCode}&dimensionLabelUnit=${item.dimensionLabelUnit}&size=105`; default: throw Error(`Unhandled item type: ${item.type}`); } }; const ChevronIcon = ({ direction }) => ( <div style={{ transform: `rotate(${direction === "up" ? "-" : ""}90deg)`, }} > ❯ </div> ); const viewerRef = React.useRef(null); const [, setUpdateTrigger] = useState({}); const forceUpdate = useCallback(() => setUpdateTrigger({}), []); // We wait for web-component to be registered useEffect(() => { customElements.whenDefined("cylindo-viewer").then(forceUpdate); }, [forceUpdate]); // We force a state change to trigger a re-render, thus observing the properties of the viewer ref. // We add our event listeners to the viewer. useEffect(() => { const viewer = viewerRef.current; if (!viewer) return; for (const eventType of EVENTS_FORCE_UPDATE) { viewer.addEventListener(eventType, forceUpdate); } return () => { for (const eventType of EVENTS_FORCE_UPDATE) { viewer.removeEventListener(eventType, forceUpdate); } }; }, [forceUpdate]); const setItemIndex = itemIndex => { viewerRef.current.item = { viewerItem: items[itemIndex], viewerItemIndex: itemIndex, }; }; // Access the properties of the viewer needed for the thumbnail bar const { items, item } = viewerRef.current || {}; const { viewerItemIndex } = item || {}; const customerId = "5098"; const code = "ARMCHAIR-PDP"; return ( <React.Fragment> <div className="wrapper"> <cylindo-viewer ref={viewerRef} customer-id={customerId} code={code}> <cylindo-studio code="RS-BARILA-A" customer-id="5098" /> <cylindo-360-frame frame="10" /> <cylindo-360 frame="3" /> <cylindo-model /> <cylindo-dimension-shot dimension-code="UXPP" unit="Cm" /> </cylindo-viewer> <div className="vertical-thumbnail-bar"> <button className="nav" disabled={!items || viewerItemIndex === 0} onClick={() => setItemIndex(viewerItemIndex - 1)} > <ChevronIcon direction="up" /> </button> {items && items.map((item, index) => ( <button key={index} className={viewerItemIndex === index ? "item selected" : "item"} onClick={() => setItemIndex(index)} > <img src={getThumbnailImgSrc({ customerId, code, item })} alt={`Thumbnail bar item - ${item.type}`} draggable="false" /> </button> ))} <button className="nav" disabled={!items || viewerItemIndex === items.length - 1} onClick={() => setItemIndex(viewerItemIndex + 1)} > <ChevronIcon direction="down" /> </button> </div> </div> </React.Fragment> ); }
The following CSS code is applied to the previous example.
.wrapper {
display: flex;
height: 400px;
background-color: rgb(246, 247, 248);
}
.wrapper cylindo-viewer {
flex: 1;
}
.vertical-thumbnail-bar {
display: flex;
flex-direction: column;
width: 70px;
}
.vertical-thumbnail-bar button {
flex: 1;
color: #435d6d;
padding: 4px;
&:not(:disabled) {
cursor: pointer;
&:hover {
border-color: #ccd5da;
}
}
}
.item {
border: 1px solid transparent;
background: #abafad26;
border-radius: 8px;
transition: border 500ms;
height: 40px;
margin: 4px;
&.selected {
border-color: #9bb0be;
}
}
.item img {
height: auto;
}
.nav {
background: none;
border-radius: 50%;
border: none;
height: 30px;
transition: background 500ms;
margin: 0px 10px;
&:disabled {
opacity: 0.5;
}
&:not(:disabled)&:hover {
background: #e7edf1;
}
}